从项目初始化到组件库开发:ai-chat-ui上手实战
第9章我们用AI完成了项目初始化,ai-chat-ui 的 Monorepo 骨架已经就位。这节直接上手,解读已有项目结构,动手开发第一个核心功能,并把 docs 包升级成 VitePress 文档站——从这节起,开发模式变成"边开发组件、边在文档站看到组件效果"。
从第9章的产出出发
第9章初始化的 ai-chat-ui 项目已经具备:
ai-chat-ui/
├── packages/
│ ├── core/ # 核心层:类型 + composables
│ ├── vue/ # Vue 组件
│ ├── markdown/ # Markdown 渲染
│ └── docs/ # 开发 playground
├── pnpm-workspace.yaml
└── package.json
text
四个包的依赖方向:core → vue → markdown,docs 是 playground。core 不依赖 Vue——将来做 React 版本时 core 层可以完全复用。
这个架构不是凭空设计的,而是基于一个核心原则:组件不关心数据从哪来,后端不关心数据怎么展示。 所有的设计决策都围绕这个原则展开。
为什么是这个项目?
三个真实理由,不讲虚的:
- AI应用UI高度定制化:流式渲染、代码高亮、思维链展示、工具调用面板,通用UI库覆盖不了
- 市场缺少后端无关方案:ai-sdk 绑定 OpenAI,chatui 绑定阿里云,我们的组件通过 ChatAdapter 接口解耦后端
- 覆盖前端核心技能80%:TypeScript 类型设计、组件 API、样式系统、测试、文档、发布,一个项目全部练到
生成结果和老师不一致怎么办
这节起进入纯开发章节,重点在开发流程和怎么配合AI工具。如果你让AI生成的代码或页面和课程的不一致,两条路:
- 让AI读老师的代码仿风格写:课程仓库的代码按阶段提交了 commit,把仓库拉下来后告诉AI"切换到某个 commit"(比如"添加 git 分支管理与 PR 工作流"这个节点),再让它对照这个阶段的代码风格继续开发
- 局部多轮调整:先让AI生成整体,再针对不一致的局部用自然语言描述差异("左侧列表两组之间的间距太大了"),多轮对话逼近预期
大体上能把流程在脑子里捋清楚就可以,不必逐像素一致。
对照主流产品拆页面:聊天界面到底有什么
写代码前先明确要落什么。打开豆包(ChatGPT、DeepSeek 同理)的对话页,表面上很简单,拆开看至少四层:
- 基础元素:左侧会话列表、中间消息流、单条消息样式、底部输入框、流式文本的实时渲染
- 弹出交互:输入框上的"+"菜单,关联到上传组件、读取浏览器 API 获取音频的语音组件、引导安装的工具组件
- 文本渲染:加粗、有序列表、行内代码、带背景的代码块——代码块上还有复制、引用、放大、查看运行等操作。这是 Markdown 文本格式的渲染输出,意味着项目要集成 Markdown 渲染(第4节的主题)
- 看不见的部分:adapter 抽象层、流式输出、按需使用——这些在 README 里已经写成项目结构说明了
需求分析的产出不需要正式的"需求原型 → 原型设计文档"全套流程,个人项目拿主流产品的页面当参考锚点,直接开工。
动手写第一个功能:ChatAdapter 接口
不按"先设计再实现"的传统流程走,直接用AI写代码。在 Claude Code 中输入:
基于 packages/core 的目录结构,设计并实现 ChatAdapter 接口。
要求:
1. sendMessage 返回 AsyncGenerator,支持流式
2. 可选的 abort 和 getHistory 方法
3. 配套 Message、StreamChunk 类型定义
text
AI 会直接读取已有的 package.json 和目录结构,生成完整的类型文件。我们只需要审查两个关键决策:
决策一:Message.metadata 为什么用宽松类型?
metadata?: Record<string, unknown>
typescript
不同后端返回不同字段(token数、模型名、finish_reason),组件不应该限定这些。放宽松了,任何后端都能接入。
决策二:ChatAdapter.sendMessage 为什么返回 AsyncGenerator?
sendMessage(options: SendMessageOptions): AsyncGenerator<StreamChunk>
typescript
因为流式是AI聊天的核心体验。AsyncGenerator 天然支持 for await...of 消费,调用方不需要处理回调或 Promise 链。而且 AsyncGenerator 是语言级别的抽象,不依赖任何框架。
让AI验证架构设计
接口写完后,让AI做架构审查:
检查 core 包的类型设计:
1. 类型是否覆盖了所有使用场景(文本/工具调用/思维链/错误/结束)
2. 有没有遗漏的字段
3. 可选属性的设计是否合理
text
这种"写完就让AI审查"的模式,比传统开发中人工 review 效率高很多。AI能快速发现遗漏的边界情况,比如"如果后端不支持流式怎么办"——这会引导你补充一个同步的 fallback 方案。
用 Vitest 验证类型正确性
类型设计好不好,测试最直观:
// core/__tests__/types.test.ts
import type { Message, StreamChunk, ChatAdapter } from '../src/types'
import { describe, it, expect } from 'vitest'
describe('Core Types', () => {
it('Message supports all roles', () => {
const user: Message = { id: '1', role: 'user', content: 'hi', createdAt: new Date() }
const assistant: Message = { id: '2', role: 'assistant', content: 'hello', createdAt: new Date() }
expect(user.role).toBe('user')
})
it('StreamChunk covers all types', () => {
const chunks: StreamChunk[] = [
{ type: 'text', content: 'hello' },
{ type: 'tool_call', content: '{"name":"search"}' },
{ type: 'thinking', content: 'let me think...' },
{ type: 'error', content: 'timeout' },
{ type: 'done', content: '' },
]
expect(chunks).toHaveLength(5)
})
})
typescript
如果 TypeScript 编译通过且测试通过,说明类型设计是自洽的。
把 docs 包升级为 VitePress 文档站
核心接口就位后,要让"边开发组件、边看到组件效果"成为可能。给 Claude Code 发第一条指令:
我想边开发核心组件,并能看到组件的实际效果,方便调试。
把项目的文档站运行起来。
text
AI 启动的是 Vite 默认的白板页面——能用但不像一个组件库项目。观察 Element Plus 这类组件库的文档站,结构是通用的:左侧 sidebar 导航,中间是 Markdown 渲染的内容,组件文档页里直接渲染真实的 Vue 组件。既然有 Vue 生态原生的 VitePress(当前 1.6.x),直接切换:
把这个 docs 站切换成为一个 VitePress 的站点,其中要有首页、组件、指南和 playground,
做成一个开源站的样子。
text
关键观察:复杂需求会自动触发 plan 模式。Claude Code 收到指令后经过长时间思考,主动产出完整计划——目录结构、涉及的组件清单、实施阶段(基础搭建 → 指南 → 组件文档 → playground → 收尾)、关键决策、最后清理旧文件。这份计划先落到根目录的 plan.md,确认后再执行。执行完毕的文档站包含指南(快速开始)、组件、composables、playground 四大板块,playground 此刻还只是简单组合,后续逐步丰富。
本课程的完整重构计划已存放在 notes/10/resources/组件库文档重构计划.md,可以直接拖给本地的 Claude Code:"按照这份 plan.md 执行"。VitePress 站点里 workspace 包走 Vite alias 指向源码,改组件代码文档站实时热更新,这就是选它而不是单独写 demo 页的原因。
功能完成记得提交。现在提交基本是一句指令的事("commit"),AI 完成 add/message/commit 全流程。
自顶向下的页面设计:ui-ux-pro-max skill
playground 页面要丰富起来。设计来源有两条路:
- AI 直接生成:用 ui-ux-pro-max 这类设计 skill。早年模型直接生成 UI 效果很差,有了专门的设计 skill 后质量已经可用
- 设计稿转换:有 Figma 设计稿时走 Figma MCP,对着设计稿操作
项目没有设计稿,走第一条。给 skill 的提示词要交代清楚布局结构和约束条件:
使用 ui-ux-pro-max 这个 skill,帮我设计:
完全重构 playground 页面的样式,需要对标主流的 Claude Code、Claude Chat、ChatGPT、
豆包 AI 的会话页面。包含左侧边栏、中间的会话区域,以及下面的输入框。
输入框需要有发送按钮以及相关的菜单按钮。
页面上先使用 mock 数据,不用考虑接口,我主要是想看看最终的效果。
其中的组件也不需要在本项目中实现,只用 skill 完成样式部分的设计。
text
一次生成后进入多轮调 bug 循环,几个真实案例:
- 左侧会话列表按时间分组后两组间距过大:描述为"左侧的列表有一个 bug,虽然按时间进行了分组,但是两个组下面的会话中间隔得很开"——自然语言描述即可,不需要术语
- hover 时列表项有跳动:这是用
display: none切换操作按钮导致的布局抖动,正确做法是visibility或透明度切换(后面 reflect 环节会把这条经验固化下来) - 缺暗黑/明亮模式切换:直接说"右侧顶部加入暗黑模式与明亮模式切换按钮"
怎么精确告诉AI改哪块DOM:两种定位手段。
- Chrome DevTools 选中元素 → copy → 粘贴给 Claude Code,它就知道你要调整的是哪一块
- 配置 Playwright MCP + 浏览器操作 skill,AI 在会话过程中直接操作浏览器查看和验证效果
这一节的分工已经说明问题:页面的视觉和布局完全可以不碰代码,靠多轮会话逼近;但组件传参、事件设计、事件传递必须读代码才能审核——完全不懂技术能生成页面,却没法验收代码,提交出来的代码经不起后续重构。
README 是项目总纲,CLAUDE.md 是AI行为约束
项目不断开发,README 却没跟着功能演进更新。开工新功能前先把总纲对齐——README 和 CLAUDE.md 分工不同:
- README:业务相关,项目开发的总领性文件——已完成功能、技术栈、结构、部署
- CLAUDE.md:约束AI行为——提交规范、开发流程、代码风格
重构 README 的提示词四要点:
按照当前项目的结构,结合之前 git 完成的功能来对 README 进行重构:
1. 说明项目的基础结构以及后续目录的组织,按照业界规范的方式
2. 把已经完成的功能特性,简要地在 README 的最上面说明
3. 在文档的最上面交代项目的技术栈与技术特点
4. 关于开发部署,要根据最新的代码来写,越详细越好
text
改写后的 README 包含:已完成功能、技术栈更新、项目结构、AI 开发规范、多阶段构建、包依赖关系、后续目录规划、快速开始、基本用法、Git hooks、Docker 部署与手动部署、包总览。
两个习惯值得坚持:每个功能开新会话,而不是在旧会话里续命——长会话浪费上下文;先提交后开发——工作区有未提交内容时不开新功能(后面会把它写进 CLAUDE.md 固化成规则)。
/reflect:把会话里的约束沉淀进 CLAUDE.md
多轮会话里你反复约束过AI很多东西(配色要求、组件结构、样式细节),下次开发同类功能难道要再重复一遍?用 /reflect 自省 skill 挖掘会话历史,把值得固化的约束写进 CLAUDE.md。
在本节的多轮样式调整后执行 /reflect,它从会话里提炼出四条经验:
- 布局用固定高度,避免内容变化引起容器跳动
- hover 显隐用
visibility替代display: none,不占位切换防抖动 - hover 背景用中性 RGBA(如
rgba(128,128,128,0.15)),明暗两种模式通吃 - 主题定制统一走 CSS Variables
多次执行 /reflect、多次更新 CLAUDE.md,AI 会越来越了解你的项目和做事风格。工作流就此变成两条线:与AI持续对话改进产出,以及在这个对话过程中持续改进AI本身。
这节课的开发节奏总结
不是"先设计架构再写代码",而是:
- 解读已有项目结构 → 理解设计意图
- 用AI直接写核心接口 → 动手优先
- 审查AI的决策 → 理解每个选择的理由
- 让AI做架构审查 → 互补验证
- 用测试验证类型 → 代码级别的确认
- 文档站 + 页面设计先行 → VitePress 落地,ui-ux-pro-max 出视觉稿
- README 对齐 + /reflect 沉淀 → 项目总纲和AI约束双更新
这是AI时代的开发节奏:动手比空想快,AI生成的代码你审查理解,而不是自己从头写;视觉靠多轮会话调,约束靠 reflect 固化。
下一节我们深入 core 包的核心实现:useChat composable 如何管理流式状态,完整实现消息发送、中断和清空的逻辑。
↑